﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace MLSharp.See5
{
	/// <summary>
	/// Used by <see cref="See5RuleSet"/> to parse a See5
	/// rules file.
	/// </summary>
	//NOTE: The See5 rule file looks like this (my comments are prefixed with *)
	//id="See5/C5.0 2.03 2008-04-22"
	// *I'm not sure what the following lines do...
	//att="ED_EYE" elts="1","4","3","2"
	//att="ED_VRB" elts="1","5","3","4","2"
	//att="ED_MTR" elts="1","6","5","4","2","3"
	//att="ED_GCS" elts="3","15","12","9","8","14","11","10","5","6","13","4","7"
	//att="ED_TSARR" elts="0","11","5","6","9","8","4","10","3","12","2","7","1"
	//entries="1"
	//rules="58" default="1"
	// *This is the start of a rule.  It says that the rule has 5 conditions, covers 370
	//  training instances, and is correct on 362 of them.
	//conds="5" cover="370" ok="362" lift="2.01688" class="0"
	// *This is an actual condition of the rule.  It says race must equal "W".
	//type="1" att="RACE" val="W"
	// *Another condition, this says that ED_RESP must be <= 15 
	//  (Note that it says less-than-or-equal, not just less-than).
	//type="2" att="ED_RESP" cut="15" result="<"
	// *Another condition, this says ED_Pulse must be > 114
	//type="2" att="ED_PULSE" cut="114" result=">"
	//type="2" att="ED_BP" cut="124" result="<"
	//type="1" att="Gender" val="0"
	internal class RuleParser
	{
		#region Private Const Fields

		/// <summary>
		/// Regex that extracts rule header information.
		/// </summary>
		private const string RULE_INFO_REGEX = "(?:conds=\\\"(?<conditionCount>\\d+)\\\"\\ cover=\\\"(?<cover>\\d+)\\\"\\ ok=\\\"(?<correct>\\d+)\\\"\\ lift=\\\"(?<lift>\\d\\.\\d+)\\\"\\ class=\\\"(?<class>.+)\")";

		/// <summary>
		/// Regex that extracts condition attributes.
		/// </summary>
		private const string CONDITION_REGEX = "(?:type=\\\"(?<type>\\d)\\\"\\ att=\\\"(?<attribute>\\w+)\\\"\\ (?:cut|val)=\\\"(?<value>(\\w|\\.)+)\\\"" +
"(?:\\ result=\\\"(?<operation>.)\\\")?)";
        
		#endregion

		#region Private ReadOnly Fields

		/// <summary>
		/// Extracts rule header information.
		/// </summary>
		private readonly Regex mRuleInfoExtractor = new Regex(RULE_INFO_REGEX, RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.IgnoreCase);

		private readonly Regex mConditionExtractor = new Regex(CONDITION_REGEX, RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.IgnoreCase);

		#endregion

		#region Private Fields

		/// <summary>
		/// The semi-readable rules.
		/// </summary>
		private readonly Rule[] mRules;

		#endregion

		#region Private Methods

		/// <summary>
		/// Gets the next rule from the specified stream.
		/// </summary>
		/// <param name="reader"></param>
		/// <returns></returns>
		private Rule GetRule(TextReader reader)
		{
			Match headerInfo = mRuleInfoExtractor.Match(reader.ReadLine());

			Rule rule = new Rule
			{ 
				Class = headerInfo.Groups["class"].Value,
				Correct = int.Parse(headerInfo.Groups["correct"].Value),
				Cover = int.Parse(headerInfo.Groups["cover"].Value),
				Lift = double.Parse(headerInfo.Groups["lift"].Value)
			};

			int conditionCount = int.Parse(headerInfo.Groups["conditionCount"].Value);

			rule.Conditions = new RuleCondition[conditionCount];

			for (int i = 0; i < conditionCount; i++)
			{
				string ruleLine = reader.ReadLine();

				try
				{
					Match conditionInfo = mConditionExtractor.Match(ruleLine);

					//TODO: Fix this, but I'm not sure how...
					rule.Conditions[i] = new RuleCondition
											{
												Attribute = conditionInfo.Groups["attribute"].Value,
												OperatorType =
													GetConditionType(int.Parse(conditionInfo.Groups["type"].Value),
																	 conditionInfo.Groups["operation"].Value),
												Value = conditionInfo.Groups["value"].Value
											};
				}
				catch (Exception ex)
				{
					throw new Exception("Unable to parse line: " + ruleLine, ex);
				}
			}

			return rule;
		}

		/// <summary>
		/// Converts from the type and operation attributes of a See5 condition
		/// to a <see cref="ConditionOperatorType"/>.
		/// </summary>
		/// <param name="typeValue">The value of the type parameter.</param>
		/// <param name="operation">The operation or null.</param>
		/// <returns>The type of the condition.</returns>
		private static ConditionOperatorType GetConditionType(int typeValue, string operation)
		{
			switch (typeValue)
			{
				case 1:
					return ConditionOperatorType.Equals;

				case 2:
					switch (operation)
					{
						case "<":
							return ConditionOperatorType.LessThanOrEqual;
						case ">":
							return ConditionOperatorType.GreaterThan;
						default:
							throw new ArgumentException("Invalid value: " + operation, "operation");
					}
				default:
					throw new ArgumentException("Invalid value: " + typeValue, "typeValue");
			}
		}

		#endregion

		#region Public Properties

		/// <summary>
		/// Gets the rules.
		/// </summary>
		public Rule[] Rules
		{
			get { return mRules; }
		}

		#endregion

		#region Public Constructors

		/// <summary>
		/// Parses the specified rules file.
		/// </summary>
		/// <param name="ruleFile">The path to the See5 rules file.</param>
		public RuleParser(string ruleFile)
		{
			using (StreamReader reader = new StreamReader(ruleFile))
			{
				//Skip lines until we have read the line that starts with "rules"
				while (!reader.ReadLine().StartsWith("rules"))
				{
				}
				
				//This stores the parsed rules.
				List<Rule> rules = new List<Rule>();

				//Now, read in the rules, line by line.
				while (!reader.EndOfStream)
				{
					rules.Add(GetRule(reader));
				}

				mRules = rules.ToArray();
			}
		}

		#endregion
	}
}
